Best Practices

Detecting Tests

In some cases generators should not transform node when they are used in tests as some of their assumptions about the structure of their input might not hold in tests. Tests often test nodes in isolation which doesn't work well with more complex generators that depend on some context.

Use the IsInTestsExpression from the com.mbeddr.mpsutil.blutil.genUtil language, which is part of the MPS-extensions:

is applicable:
    (genContext) -> boolean {
        !is-in-tests;
    }

Or alternatively:

--- is applicable ---
(genContext)->boolean {
  !genContext.originalModel.nodes(<all>).any({~it => it.concept.getLanguage().getQualifiedName().startsWith("jetbrains.mps.lang.test"); });
}

If you use the later it might make sense to move this code into a helper class and use it that in the various places of your code bases.

Preprocessing

In scenarios where the structure of the input model is significantly different form the output structure it is often handy to use preprocessing scripts written in Java rather than reduction or weaving rules. Another sometimes useful benefit is that debugging Java code can be easier than the declarative and interpreted rules of the generator.

An example where preprocessing can make a lot of sense is inout collection for generation. If the generation target is for instance a XML file with a specific structure but the input model allows the user to freely place these elements preprocessing can collect all these contents and place them under a single node with a structure closer to the one of output.

In addition to using preprocessing for to change the structure of the input it is also handy to have intermediate language who's sole purpose is to ease generation. These languages often contain concepts that user does not need to specify explicitly in the input because they can be derived from the input. But during generation it often simplifies the generators if these elements are explicitly in the model because they can be generated by simple means of reduction rules. An example for this would be a generator that generates serialisation logic for data structures but could derive the data types for certain input automatically (e.g. boolean). The actual generator to produce the serialisation logic is much simpler to write when these data types are explicitly in the model. In this case the preprocessing would add these information.

Preprocessing comes at a cost, as it's not as declarative as the generator language tracing has to be done manually via the TracingUtils fillOriginalNode. Also registering inputs and outputs to mapping label needs to be done explicitly via: genContext.label input to output as myLabel;. The generator is also not able to execute anything concurrently while a script is used. The usage of preprocessing scripts that heavily modify the model should be kept to a minimum.

Error Handling

When reporting errors during generation the natural thing to use is exceptions as most of the code that is written in MPS generators is Java. But doing so has some disadvantages.

  • It stops generation immediately. The user only get's single error message if there are multiple errors during generator the user can only fix the first, then has to regenerated, get the next error and start over. This can be a very frustrating process.
  • There is no way to point the user to the input that caused the error. The exception will contain a link to the rule that cause it but there is not additional information useful for the user to debug. If transient models are turned on the user might be able to see the intermediate state which caused the error but it's often hard to guess from the which node in the original model caused it.

It is much better do something like this:

genContext.show error "something went wrong" -> genContext.get original copied input for (node)

This will stop generation after the current generation phase is complete. No other generators will execute afterwards, but it will collect all errors from the currently executed generator. It also allows to specify a node where the user is taken to when the messages is clicked. Together with the genContext.get original copied input for (node) pattern it takes the user directly to the input in the original model.

Copy and Reduce

When generating output from a list of items or a single child node the first idea is often to use a $LOOP$ macro or to do the transformation in place. While this looks easy in the first place it also limits extensibility. It is often much better to avoid this kind of pattern and use a combination for $COPY_SRC$/$COPY_SRCL$ and a reduction rule for the concept. This allows extensions to contribute their own reduction rules for their concepts. Incase a $LOOP$ macro would be used the only option to extend the generator would be to essentially copy the complete mapping and have the generator run before the one of the extended language is invoked.

Here is an example from the mpsutil codebase:

new AfterExtension("$wizId", "$stepId", new arraylist<AbstractWizardStepEx>{$LOOP$new ->$dummy_step()})

It assumes that all elements of the arraylist are created by invoking a constructor of a class. If somebody wants to extend this behaviour and wants to include a singleton object into the list it's impossible. The code has been rewritten to:

new AfterExtension("$wizId", "$stepId", new arraylist<AbstractWizardStepEx>{$COPY_SRCL$new dummy_step()})

// and a reduction rule for the steps

concept Step       --> <T  new ->$dummy_step()  T> 

inheritors true                                     
condition <always>                                  

Another pattern to avoid this limitation is to use a $LOOP$ macro but delegate the actual reduction into a template switch by calling it with a. $SWITCH$ macro:

new ConceptEvaluatorBase(concept/->$ConceptEvaluator/, $true, $LOOP$$SWITCH$ populateConstraintconstraints) { 

Prefer Switches over Ifs

In cases where the generator should produce output based on a condition the common macro to use is usually the $IF$ macro. There is nothing wrong with this per se but in cases where this condition is based on a model element other than a boolean property it is usually a smell that it should be replaced with a template switch.

The following example is from the mebddr codebase.

$IF$return $COPY_SRC$null; / 
$ELSE$<T  $COPY_SRCL$return;  T>

--- inspector --- 
conditional branch                                                                                                    

comment : <none>                                                                                                      
mapping label : <no label>                                                                                            
condition : (genContext, node, operationContext)->boolean { 
  node.evaluator.isInstanceOf(ConceptEvaluatorInline); 
}
alternative : <T  $COPY_SRCL$return;  T>                                                                              

The code essentially checks what kind of concept is in the evaluator child role and then changes the way in which it generates the code. It handles the two cases that were assumed in the original language perfectly well, but in case we want to introduce a custom Evaluator it would fail.

is better rewritten like this:


// replacment for the $IF$

$SWITCH$ evaluatorImplementationreturn null;

--- inspector ---

switch templates by input node                                                  

comment : <none>                                                                
mapping label : <no label>                                                      
use input : (genContext, node, operationContext)->node<> { 
  node.evaluator; 
}

template switch : evaluatorImplementation                                       

// the template switch

template switch evaluatorImplementation extends <none>                  

parameters                                                              
<< ... >>                                                               

  null-input message: <none>                                            

  cases:                                                                

     concept ConceptEvaluatorInline --> <T  return $COPY_SRC$null;  T> 

     inheritors true                                                    
     condition <always>                                                 
     concept ConceptEvaluatorBody --> <T  $COPY_SRCL$return;  T> 

     inheritors true                                                    
     condition <always>                                                 


  default: DISMISS TOP RULE error : can not handle evaluator            


Note that the external template switch is extensible from other generators. By simply defining a extends relationship: template switch mySwitch extends evaluatorImplementation. This will contribute the additional cases each time the original switch is invoked.

In addition the original template switch, if it doesn't have a default case, wants to print an error message. Because otherwise the template switch invocation is replaced with node that has the $SWITCH$ macro attached. This is done via a default rule that looks like this:

default: DISMISS TOP RULE error : can not handle evaluator

Predefined Generator Plans

In some cases defining a static generation plan can be very useful and simplify the view on which generators are engaged at which point during the generation. Though this is mostly the last option one want to take. At the moment the extensibility story for predefined generator plans is pretty limited compared to the dynamic approach with priorities. Currently you can only contribute generators to the predefined plan via extension dependency to a generator that is involved in the generation plan this generator is the executed in the same step as the extended generator. It can also cause problems in conjunction with node/editor tests. If a DevKit with a generation plan and this DevKit is used in tests, this can cause that MPS would not consider the generators of the test language. As mentioned above this is a solution that currently doesn't widely apply especially not if your languages are meant to be extended. But in some contexts where extensibility is not the main concern or even undesired using predefined generation plans can help.